package org.xpect.parameter;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.xtext.util.Pair;
import org.eclipse.xtext.util.Strings;
import org.eclipse.xtext.util.formallang.FollowerFunctionImpl;
import org.eclipse.xtext.util.formallang.Nfa;
import org.eclipse.xtext.util.formallang.NfaUtil;
import org.eclipse.xtext.util.formallang.NfaUtil.BacktrackHandler;
import org.eclipse.xtext.util.formallang.ProductionUtil;
import org.eclipse.xtext.util.formallang.StringProduction;
import org.eclipse.xtext.util.formallang.StringProduction.ProdElement;
import org.junit.ComparisonFailure;
import org.xpect.XpectImport;
import org.xpect.XpectInvocation;
import org.xpect.XpectReplace;
import org.xpect.expectation.impl.TargetSyntaxSupport;
import org.xpect.runner.ArgumentContributor;
import org.xpect.setup.XpectSetupFactory;
import org.xpect.state.Configuration;
import org.xpect.text.IRegion;
import org.xpect.text.Region;
import com.google.common.collect.Maps;
@SuppressWarnings("restriction")
@XpectSetupFactory
@XpectReplace(ArgumentContributor.class)
@XpectImport({ StringProvider.class, IntegerProvider.class, OffsetProvider.class })
public class ParameterParserImpl extends ArgumentContributor {
public static class AssignedProduction extends StringProduction {
public AssignedProduction(String production) {
super(production);
}
@Override
protected ProdElement parsePrim(Stack<Pair<Token, String>> tokens) {
Pair<Token, String> current = tokens.pop();
switch (current.getFirst()) {
case PL:
ProdElement result1 = parseAlt(tokens);
if (tokens.peek().getFirst().equals(Token.PR))
tokens.pop();
else
throw new RuntimeException("')' expected, but " + tokens.peek().getFirst() + " found");
parseCardinality(tokens, result1);
return result1;
case STRING:
ProdElement result2 = createElement(ElementType.TOKEN);
result2.setValue(current.getSecond());
parseCardinality(tokens, result2);
return result2;
case ID:
ProdElement result3 = createElement(ElementType.TOKEN);
result3.setName(current.getSecond());
Pair<Token, String> eq = tokens.pop();
if (eq.getFirst() == Token.EQ) {
Pair<Token, String> val = tokens.pop();
switch (val.getFirst()) {
case ID:
case STRING:
result3.setValue(val.getSecond());
break;
default:
throw new RuntimeException("Unexpected token " + current.getFirst());
}
} else
throw new RuntimeException("Unexpected token " + eq.getFirst() + ", expected '='");
parseCardinality(tokens, result3);
return result3;
default:
throw new RuntimeException("Unexpected token " + current.getFirst());
}
}
}
protected static class BacktrackItem {
protected int offset;
protected IRegion region;
protected ProdElement token;
public BacktrackItem(int offset) {
super();
this.offset = offset;
}
public BacktrackItem(int offset, ProdElement token, IRegion region) {
super();
this.offset = offset;
this.token = token;
this.region = region;
}
@Override
public String toString() {
return token + ":" + region;
}
}
public enum Token {
ID("[a-zA-Z][a-zA-Z0-9_]*"), // String
INT("[0-9]+"), // Integer
OFFSET("'([^']*)'|[^\\s]+"), // Integer, returns position of first occurrence of pattern below the test
STRING("'([^']*)'"), // String, returns the string inside the quotes.
TEXT("[^\\s]+"); // String, similar to ID but accepts more characters, i.e. anything that is not whitespace
public final Pattern pattern;
private Token(String regex) {
this.pattern = Pattern.compile("^" + regex);
}
}
protected static final Pattern WS = Pattern.compile("^[\\s]+");
private final ParameterParser annotation;
private final ParameterRegion parameterRegion;
private final XpectInvocation statement;
private final TargetSyntaxSupport syntax;
public ParameterParserImpl(TargetSyntaxSupport syntax, XpectInvocation statement) {
this.annotation = statement.getMethod().getJavaMethod().getAnnotation(ParameterParser.class);
this.parameterRegion = statement.getRelatedRegion(ParameterRegion.class);
this.statement = statement;
this.syntax = syntax;
}
@Override
public void contributeArguments(Configuration[] configurations) {
if (annotation == null || Strings.isEmpty(annotation.syntax()))
return;
AssignedProduction syntax = new AssignedProduction(annotation.syntax());
Map<String, IStatementRelatedRegion> parsed = parseParams(annotation.syntax(), syntax, statement, parameterRegion);
Map<String, Token> defaults = getDefaults(syntax);
for (int i = 0; i < configurations.length; i++) {
String key = "arg" + i;
IStatementRelatedRegion value = parsed.get(key);
if (value != null) {
configurations[i].addDefaultValue(value);
} else {
Token token = defaults.get(key);
if (token != null) {
IStatementRelatedRegion default1 = convertDefault(token);
configurations[i].addDefaultValue(default1);
}
}
}
}
protected Map<String, Token> getDefaults(AssignedProduction syntax) {
Map<String, Token> result = Maps.newHashMap();
for (ProdElement ele : new ProductionUtil().getAllChildren(syntax, syntax.getRoot())) {
String name = ele.getName();
if (!Strings.isEmpty(name))
result.put(name, Token.valueOf(ele.getValue()));
}
return result;
}
protected IStatementRelatedRegion convertDefault(Token token) {
switch (token) {
case OFFSET:
int offset = syntax.findFirstSemanticCharAfterStatement(statement);
return new OffsetRegion(parameterRegion, offset);
case INT:
return new IntegerRegion(parameterRegion);
case STRING:
case TEXT:
case ID:
return new StringRegion(parameterRegion);
}
throw new RuntimeException();
}
protected IStatementRelatedRegion convertValue(XpectInvocation invocation, Token token, IRegion claim, IRegion match) {
switch (token) {
case OFFSET:
return new OffsetRegion(parameterRegion, claim.getOffset() + match.getOffset(), match.getLength());
case INT:
return new IntegerRegion(parameterRegion, claim.getOffset() + match.getOffset(), match.getLength());
case STRING:
case TEXT:
case ID:
return new StringRegion(parameterRegion, claim.getOffset() + match.getOffset(), match.getLength());
}
throw new RuntimeException();
}
public ParameterParser getAnnotation() {
return annotation;
}
protected Nfa<ProdElement> getParameterNfa(AssignedProduction prod) {
FollowerFunctionImpl<ProdElement, String> ff = new FollowerFunctionImpl<ProdElement, String>(prod);
ProdElement start = prod.new ProdElement(StringProduction.ElementType.TOKEN);
ProdElement stop = prod.new ProdElement(StringProduction.ElementType.TOKEN);
Nfa<ProdElement> result = new NfaUtil().create(prod, ff, start, stop);
return result;
}
protected Map<String, IStatementRelatedRegion> parseParams(String syntax, AssignedProduction production, XpectInvocation invocation, IRegion claim) {
String document = invocation.getFile().getDocument();
final String text = document.substring(claim.getOffset(), claim.getOffset() + claim.getLength());
Nfa<ProdElement> nfa = getParameterNfa(production);
Matcher ws = WS.matcher(text);
List<ParameterParserImpl.BacktrackItem> trace = new NfaUtil().backtrack(nfa, new BacktrackItem(ws.find() ? ws.end() : 0),
new BacktrackHandler<ProdElement, ParameterParserImpl.BacktrackItem>() {
public ParameterParserImpl.BacktrackItem handle(ProdElement state, ParameterParserImpl.BacktrackItem previous) {
if (Strings.isEmpty(state.getValue()))
return new BacktrackItem(previous.offset, state, new Region(text, previous.offset, 0));
if (Strings.isEmpty(state.getName())) {
if (text.regionMatches(previous.offset, state.getValue(), 0, state.getValue().length())) {
int newOffset = previous.offset + state.getValue().length();
Matcher ws = WS.matcher(text).region(newOffset, text.length());
int childOffset = ws.find() ? ws.end() : newOffset;
return new BacktrackItem(childOffset, state, new Region(text, previous.offset, state.getValue().length()));
}
} else {
Token t = Token.valueOf(state.getValue());
Matcher matcher = t.pattern.matcher(text).region(previous.offset, text.length());
if (matcher.find()) {
Matcher ws = WS.matcher(text).region(matcher.end(), text.length());
int childOffset = ws.find() ? ws.end() : matcher.end();
int start, end;
if (matcher.groupCount() > 0 && matcher.group(1) != null) {
start = matcher.start(1);
end = matcher.end(1);
} else {
start = matcher.start(0);
end = matcher.end(0);
}
return new BacktrackItem(childOffset, state, new Region(text, start, end - start));
}
}
return null;
}
public boolean isSolution(ParameterParserImpl.BacktrackItem result) {
return true;
}
public Iterable<ProdElement> sortFollowers(ParameterParserImpl.BacktrackItem result, Iterable<ProdElement> followers) {
return followers;
}
});
Map<String, IStatementRelatedRegion> result = Maps.newHashMap();
String trimmed = text.trim();
if (trace != null && !trace.isEmpty()) {
IRegion last = trace.get(trace.size() - 1).region;
int end = last.getOffset() + last.getLength();
if (end < claim.getLength()) {
String trailing = text.substring(end).trim();
if (!trailing.isEmpty())
errorLeftoverTrailingText(claim, end, trailing, syntax);
}
for (ParameterParserImpl.BacktrackItem item : trace) {
if (item.token != null && item.token.getName() != null) {
String key = item.token.getName();
Token token = Token.valueOf(item.token.getValue());
result.put(key, convertValue(invocation, token, claim, item.region));
}
}
return result;
}
if (trimmed.isEmpty())
throw new RuntimeException("Parameters that match syntax \"" + syntax + "\" are required, but have not been provided.");
throw new RuntimeException("could not parse \"" + trimmed + "\" with grammar \"" + syntax + "\"");
}
protected void errorLeftoverTrailingText(IRegion claim, int offset, String leftover, String syntax) {
String expected = claim.getDocument().toString();
String actual = expected.substring(0, claim.getOffset() + offset) + expected.substring(claim.getOffset() + claim.getLength());
String text = claim.getRegionText().trim();
String msg = "When parsing \"" + text + "\" with syntax \"" + syntax + "\", the trailing part \"" + leftover + "\" remains unmatched.";
throw new ComparisonFailure(msg, expected, actual);
}
}